5. Wprowadzenie do JavaScriptu

Wyzwania:

  • dowiesz się, czym jest algorytm,
  • poznasz podstawowe pojęcia z zakresu JavaScriptu,
  • nauczysz się, jak stworzyć prostą grę.

5.1. Czym jest JavaScript?

Zaczynamy naszą przygodę z JavaScriptem! Twoje projekty, do tej pory statyczne i reagujące co najwyżej na ruch kursorem, będą mogły nabrać życia i zaoferować użytkownikom nowe funkcjonalności!

Wprowadzenie

Profesor uniwersytetu z pewnością mógłby udzielić Ci bardzo głębokiej odpowiedzi na pytanie "czym jest JavaScript?", ale my nie będziemy Cię zanudzać genezą JS-a i zawiłą terminologią. Szczególnie że zapewne nie możesz się już doczekać, kiedy zaczniesz pisać własne skrypty! ;)

Musimy jednak odpowiedzieć sobie na pytanie: czym dla nas jest JavaScript?

Metaforycznie, jest to taki "robot" żyjący w przeglądarce. Za pomocą JavaScriptu możesz mu wytłumaczyć, co ma robić. Może wykonywać skomplikowane operacje na liczbach, "na żywo" zmieniać HTML na stronie, czy łączyć się z serwerem w internecie, aby pobrać z niego jakieś informacje i wyświetlić użytkownikowi.

Podobnie jak metoda gumowej kaczuszki, ta metafora może wydawać się dziecinna, ale jest bardzo przydatna. Pozwala lepiej zrozumieć, że nasz kod JS to nic innego, jak "rozkazy" dla tego "robota".

Możesz spodziewać się kolejnych metafor w tym module. Wszystkie będą tworzyć większą całość, czyli pozwolą nam na stopniowe rozszerzanie tej analogii o kolejne elementy. Dzięki temu łatwiej będzie Ci poszerzać wiedzę o nowe zagadnienia.

Dla ułatwienia i zwiększenia czytelności, kolejne porównania będą zawarte w ramkach z nagłówkiem "Metafora".

Myślenie algorytmiczne

Zanim dotkniemy pierwszej linijki kodu JS, musimy wyjaśnić kwestię myślenia algorytmicznego. Skrypty JavaScript to nic innego, jak zapis algorytmu w pewnym konkretnym języku. O ile łatwo możesz znaleźć mnóstwo informacji na temat samego języka, o tyle trudniej jest nauczyć się myślenia algorytmicznego. Najpierw jednak musimy zrozumieć, czym jest algorytm.

Definicja algorytmu

Algorytm – skończony ciąg jasno zdefiniowanych czynności, koniecznych do wykonania pewnego rodzaju zadań. Sposób postępowania prowadzący do rozwiązania problemu.

Źródło: Wikipedia

Z tej definicji wynika, że:

  • algorytm musi mieć pewną (skończoną) liczbę kroków,
  • kroki muszą być jasno zdefiniowane,
  • cel algorytmu to wykonanie jakiegoś zadania.

Od razu możemy zwrócić uwagę, że jest to bardzo ogólna definicja. Np. nie wyklucza ona, że algorytm może się składać z algorytmów... Jednak do tego tematu jeszcze wrócimy! ;)

Słowo "algorytm" może początkowo Cię trochę przytłaczać, ponieważ może Ci się wydawać, że to coś skomplikowanego. W takich chwilach wystarczy, że przypomnisz sobie: przepis z książki kucharskiej jest świetnym przykładem algorytmu – jest to algorytm pieczenia ciasta!

W przeciwieństwie do książki kucharskiej nie będziemy opisywać algorytmów słownie, tylko za pomocą schematów blokowych. Nie musisz jednak uczyć się, jak je odczytywać – będziemy tworzyć je krok po kroku, więc bez problemu domyślisz się, co oznaczają.

Przykłady i ćwiczenia w tym rozdziale mogą wydawać Ci się śmieszne. Pozwalają one jednak na wytrenowanie u siebie umiejętności, które znacznie ułatwią pisanie skryptów. Gorąco zachęcamy do podjęcia próby zmierzenia się z tym wyzwaniem!

Zacznijmy od pokazania algorytmu na przykładzie zadania "zrobić herbatę". Możesz pomyśleć, że przecież każdy to wie, i powiedzieć "idź do kuchni, zagotuj wodę i zalej herbatę".

image

Jeśli jednak wyobrazisz sobie, że tłumaczysz to samo zadanie dziecku, które nigdy wcześniej nie robiło herbaty, schemat ten może być bardziej szczegółowy.

image

Teraz już mamy przedstawione wszystkie kroki, ale czasem w algorytmie występują też momenty (decyzje), w których ścieżka algorytmu się rozdwaja. Dodajmy w takim razie sprawdzenie, czy w czajniku jest woda.

image

A co się stanie, kiedy spróbujemy to samo podejście zastosować dla sprawdzania, czy herbata już się zaparzyła? Wtedy również zastosujemy decyzję, ale jedna z odpowiedzi będzie nas kierowała do wcześniejszego kroku. W ten sposób otrzymamy pętlę, w której będziemy krążyć tak długo, aż zostanie spełniony warunek.

image

Teraz nasz algorytm zrobił się już nieco skomplikowany. Dlatego dobrze by było wydzielić z niego procedury, które będą osobno zdefiniowane. Dzięki temu możemy bardziej skomplikowane elementy zapisać w uproszczonej formie, a szczegóły takiej procedury opisać osobno. Zobaczmy to na przykładzie:

image image image

Teraz widzisz, że nasz główny algorytm nie różni się bardzo od pierwotnego, prostego przykładu. Dzięki temu łatwo jest zrozumieć cały algorytm na poziomie ogólnym, a dopiero potem zagłębić się w szczegóły procedur.

Po co nam algorytmy?

Jak wspomnieliśmy na początku rozdziału, skrypt jest algorytmem zapisanym w języku JavaScript. Dlatego warto trenować u siebie myślenie algorytmiczne.

Spróbuj raz dziennie znaleźć jakiś przykład algorytmu w codziennym życiu i rozbić go na kolejne kroki, zauważyć w nim decyzje, pętle i procedury. Możesz rozpisać je za pomocą schematów blokowych albo po prostu przemyśleć algorytm pod tym kątem. Warto jednak przez kilka tygodni wykonywać takie ćwiczenie, np. w drodze do pracy.

Jak już pewnie się domyślasz, w JavaScripcie będziemy bardzo często wykorzystywać to, co w algorytmach nazwaliśmy decyzjami, pętlami i procedurami. Jednak ich wykorzystanie będzie wymagało myślenia algorytmicznego, w tym rozumienia problemu na różnych poziomach – od ogólnego, który można naraz "ogarnąć" umysłem i zaplanować potrzebne procedury, aż po szczegółowe, które pozwolą na wypełnienie procedur kodem realizującym ich zadania.

Pierwszy skrypt — hello world!

Podobnie jak wcześniej, w treści modułu będziemy wykorzystywać zagnieżdżone edytory CodePen. Pozwolą nam pokazywać na przykładach omawiane zagadnienia. Dzięki nim będziesz mieć również możliwość łatwego eksperymentowania – zmiany w kodzie zagnieżdżonych edytorów nie są nigdzie zapisywane, więc możesz dowolnie je zmieniać, bez obawy, że coś zepsujesz.

Jak zwykle w nauce programowania, zaczynamy od skryptu "Hello world".


Ten fragment kodu nie jest bardzo skomplikowany, ale już na jego podstawie możemy dowiedzieć się kilku podstawowych rzeczy:

  • kod JS jest wykonywany od góry do dołu,
  • document.write wstawia tekst (lub kod HTML) na stronę,
  • w nawiasach musimy podać tekst, który ma być wstawiony,
  • teksty zamykamy w cudzysłowach 'pojedynczych' lub "podwójnych" (zależnie od preferencji – my wolimy pojedyncze),
  • na końcu każdej linii kodu znajduje się średnik (potem dowiesz się, że są wyjątki od tej zasady).

Ćwiczenie

W powyższym edytorze dopisz jeszcze dwie linie kodu JS, które wstawią kod HTML na stronę. Sprawdź, czy w ten sposób możemy dodawać np. obrazki (już wiesz jak) lub wideo z YouTube (wystarczy, że pod video na YouTube klikniesz "Udostępnij", a następnie "Umieść", aby zobaczyć kod HTML do wstawienia na stronie).

Nie bój się JS-a!

W podejściu akademickim zwykle zaczyna się od wyjaśniania pojęć, a dopiero później ich wykorzystanie w praktyce. Nasze doświadczenie pokazuje, że taka droga jest o wiele dłuższa i z tego względu możliwie szybko zaczynamy praktykę, a potem uzupełniamy wiedzę teoretyczną.

Oznacza to, że początkowo nie będziesz rozumieć wszystkiego, co się dzieje w kodzie. Pamiętaj, że na tym etapie to absolutnie normalne! Warto jednak przyzwyczaić się do tego, że niektórych rzeczy trzeba się po prostu domyślać.

W powyższym przykładzie skryptu nie wytłumaczyliśmy, jak działa document.write i dlaczego po nim wstawiamy tekst w nawiasach. Nie jest to jednak dla Ciebie przeszkodą w tym, żeby już teraz rozumieć, co robi ten kod, a nawet go zmodyfikować, prawda?

Jako web developer będziesz często spotykać się z sytuacjami, kiedy nie rozumiesz każdego elementu kodu. Ważne, żeby domyślać się, co ten kod robi, i jak można znaleźć i zmienić kluczowy fragment kodu.

Dlatego nie przejmuj się brakami wiedzy – z czasem wyjaśnimy większość z tych niedopowiedzeń. Na razie zależy nam na tym, aby jak najszybciej zaznajomić Cię z JS-em i pozwolić Ci na pisanie kodu.

Samodzielne eksperymenty to świetny sposób na naukę. Oprócz wykonywania ćwiczeń i zadań możesz dowolnie eksperymentować z przykładami zawartymi w tym module. Pamiętaj, aby zmieniać coś w każdym przykładzie, żeby lepiej zrozumieć, jak działa.

Dlatego śmiało eksperymentuj i nie bój się zmieniać kod JS, nawet jeśli nie wszystko jest dla Ciebie jasne!

5.2. Piszemy grę!

W ramach tego modułu stworzymy prostą grę – papier, kamień, nożyce. Brzmi ambitnie, jak na początek nauki JavaScriptu? Nie przejmuj się, napisanie tej gry będzie bardzo proste, a na tym przykładzie będzie Ci łatwiej zrozumieć nowe zagadnienia.

Pierwszym krokiem do napisania jakiegokolwiek kodu jest opisanie jego funkcjonalności. Zacznijmy więc od przypomnienia zasad gry.

Zasady gry

Zapewne znasz zasady tej gry, ale nawet w takiej sytuacji dobrze jest jasno sprecyzować oczekiwany efekt.

Pojedyncza runda gry polega na jednoczesnym (w naszym wypadku – pozornie jednoczesnym) zagraniu dwóch graczy.

Każdy gracz może wybrać jeden z ruchów: papier, kamień, lub nożyce.

W naszym wypadku będzie jeden gracz, a rolę drugiego gracza będzie pełnił skrypt. Potocznie mówi się, że gramy z komputerem, więc tak też będziemy się odnosić do tego wyimaginowanego przeciwnika.

Wynik rundy może być następujący:

  • remis – jeśli obaj gracze wybrali ten sam ruch,
  • wygrana gracza, który zagrał papier, jeśli drugi gracz zagrał kamień,
  • wygrana gracza, który zagrał kamień, jeśli drugi gracz zagrał nożyce,
  • wygrana gracza, który zagrał nożyce, jeśli drugi gracz zagrał papier.

Gra toczy się do osiągnięcia przez jednego z graczy wcześniej ustalonej liczby wygranych rund.

Specyfikacja projektu

Docelowo chcielibyśmy, aby całość działała w następujący sposób:

  • na ekranie znajdują się trzy guziki: "papier", "kamień", "nożyce", umożliwiające graczowi wybór zagrania,
  • po kliknięciu któregoś z tych trzech guzików, skrypt losuje swoje zagranie,
  • na końcu podawany jest wynik gry.

Będziemy rozwijać naszą grę etapami – zaczniemy od bardzo prostej wersji, której nawet nie można nazwać grą, i stopniowo będziemy dodawać kolejne funkcjonalności.

W pierwszym etapie nasza gra będzie:

  • uruchamiać się od razu po wyświetleniu strony,
  • wyświetlać na ekranie tekst "Zagrałem kamień! Jeśli Twój ruch to papier, to wygrywasz!"

Jak wspomnieliśmy, tego nawet nie można nazwać grą, ale będzie to świetny punkt wyjścia!

Przygotowanie nowego projektu

Korzystając z wiedzy zdobytej w poprzednim module, przygotuj środowisko nowego projektu o nazwie javascript-prework.

  1. Stwórz na GitHubie nowe repozytorium pod nazwą javascript-prework.
  2. Sklonuj stworzone repozytorium.
  3. Skopiuj plik package.json stworzony w poprzednim module do katalogu projektu.
  4. Zainicjuj projekt za pomocą komendy npm run init-project uruchomionej w katalogu projektu.
  5. Zapisz commit i wyślij go (wykonaj push) do zdalnego repozytorium.
  6. Sprawdź w panelu GitHub czy pliki są widoczne w repozytorium.

W czasie realizacji tego modułu będziesz aktualizować pliki projektu. Efektem będzie projekt z działającą grą.

Nie martw się tym, że to jest Twoje pierwsze spotkanie z JavaScriptem. Niniejszy moduł pomoże Ci poznać podstawy. Aby było to możliwe, przygotowaliśmy dla Ciebie początkową zawartość paru plików, które ułatwią Ci uruchomienie projektu.

  • index.html – zastąp zawartość swojego pliku index.html kodem przygotowanym przez nas (poniżej); umieściliśmy w nim odwołania do plików CSS oraz JS, a także podstawową strukturę HTML, z której będziemy korzystać w tym module;
  • functions.js – umieść ten plik w katalogu js swojego projektu i wklej do niego zawartość podaną poniżej; znajduje się w nim kod JS niezbędny do uruchomienia przykładów z tego modułu;

Kod JS tworzony w ramach projektu będziesz umieszczać w pliku js/script.js, który został już stworzony w Twoim projekcie za pomocą komendy npm run init-project.

To wszystko, co będzie Ci potrzebne do realizacji tego modułu. Po przygotowaniu projektu zapisz nowy commit i przejdź do nauki JS-a w następnym submodule!

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Kamień, papier, nożyce</title>
	<link rel="stylesheet" href="css/style.css">
</head>
<body>
	<div class="container">
		<div id="buttons"></div>
		<div id="messages"></div>
	</div>
	<script src="js/functions.js"></script>
	<script src="js/script.js"></script>
</body>
</html>

js/functions.js

function printMessage(msg){
	let div = document.createElement('div');
	div.innerHTML = msg;
	document.getElementById('messages').appendChild(div);
}

function clearMessages(){
	document.getElementById('messages').innerHTML = '';
}

5.3. Wyświetlanie tekstu

Wiemy już, jaki będzie pierwszy etap tworzenia naszej gry, więc nie pozostaje nam nic innego, jak wziąć się do pracy!

Wywołanie funkcji

W naszym kodzie HTML przygotowaliśmy pusty kontener na wyświetlane komunikaty: <div id="message"></div>. Aby wyświetlić w nim tekst za pomocą JavaScriptu, musimy odnaleźć ten element oraz zmienić jego zawartość.


Przypomnij sobie rozdział dotyczący algorytmów – pokazaliśmy w nim decyzje, pętle i procedury. Zaczniemy od tych ostatnich, czyli procedur. Jak wspomnieliśmy wcześniej, są to fragmenty algorytmu, które zostały wydzielone, aby algorytm był bardziej czytelny. Wydzielenie procedury pozwala też na jej wielokrotne wykorzystanie.

W JavaScripcie "procedury" nazywamy funkcjami. Na potrzeby tego projektu stworzyliśmy dla Ciebie funkcję printMessage, którą dodaliśmy przed chwilą do pliku js/functions.js. Na razie nie musisz rozumieć kodu tej funkcji – omówimy to później. Ważne, że wywołanie tej funkcji będzie wyświetlać komunikat na stronie, a konkretniej w elemencie o id równym messages.

Dlaczego nie używamy document.write?

W pierwszym skrypcie "Hello World!" użyliśmy funkcji document.write do wyświetlenia napisu na stronie. Ta funkcja dodaje podany jej tekst (lub kod HTML) na końcu body.

W naszej grze to nie będzie odpowiednie rozwiązanie. Będzie nam zależało, aby tekst był dodawany w konkretnym divie, którego zawartość w razie potrzeby będziemy mogli wyczyścić, usuwając wszystkie dodane komunikaty.

Właśnie dlatego będziemy korzystać z funkcji printMessage.

Spójrz teraz na linię kodu, której użyliśmy powyżej:

printMessage('Zagrałem kamień! Jeśli Twój ruch to papier, to wygrywasz!');

Taką linię kodu nazywamy wywołaniem funkcji i robi ona dwie rzeczy:

  • uruchamia funkcję printMessage,
  • przekazuje tej funkcji tekst, który ma zostać wyświetlony.

To, co przekazujemy funkcji, nazywamy jej argumentem. Wywołanie funkcji może mieć więcej argumentów, lub nie mieć żadnych.

// wywołanie funkcji bez argumentów
nazwaFunkcji();

// wywołanie funkcji z jednym argumentem
nazwaFunkcji(argument);

// wywołanie funkcji z kilkoma argumentami
nazwaFunkcji(argument1, argument2, argument3);

Przy okazji w powyższym kodzie pokazaliśmy, w jaki sposób w JS-ie można dodawać komentarze. Wszystko od // do końca linii jest ignorowane przez JS, więc możemy tam wpisać, co chcemy.

Łączenie tekstów

Wyświetlamy już tekst na stronie, ale na razie wpisaliśmy ten tekst "na sztywno" w kodzie. Już za chwilę skrypt będzie musiał wyświetlać różne teksty, w zależności od:

  • zagrania gracza,
  • wylosowanego zagrania komputera,
  • wyniku rundy (czyli porównania ruchów gracza i komputera).

Pierwszym krokiem do tego celu jest łączenie tekstów ze sobą. Zacznijmy od powtórzenia poprzedniego przykładu, ale tym razem wydzielając nazwę zagrania komputera:


Jak widzisz w powyższym kodzie JS, możemy połączyć ze sobą dwa teksty za pomocą znaku plusa, ale każdy tekst musi być zamknięty w cudzysłowach.

Ta zmiana jeszcze nie wniosła zbyt wiele, ale za chwilę będziemy łączyć ze sobą teksty zapamiętane w zmiennych, a to już będzie znaczny postęp!

Zmienne

Pisząc skrypty JS, bardzo często będziemy chcieli zapamiętać jakąś wartość (np. tekst lub liczbę), aby móc wykorzystać ją wielokrotnie. Właśnie do tego służą zmienne! Być może pamiętasz ten termin z poprzednich modułów: używając składni Sass, mogliśmy zdefiniować np. kolory jako zmienne i używać ich później w naszym kodzie. Zmienne w JS działają na bardzo podobnej zasadzie.

Metafora – zmienna

Zmienną możesz wyobrazić sobie jako pudełko z etykietą. Na etykiecie jest napisana nazwa tego pudełka.

Pamiętasz, że JS porównaliśmy do robota, któremu wydajemy polecenia? Nie chcemy, żeby się pogubił, i dlatego każde pudełko będzie miało inną etykietę.

Później dowiesz się, że pudełka w różnych pokojach mogą mieć takie same etykiety, ale na razie się tym nie przejmuj.

Pudełko może być na początku puste, ale zwykle będziemy w nim chcieli coś przechowywać. Dla pudełka nie ma różnicy, co w nim będzie, tzn. nie mamy np. innych pudełek na liczby niż na teksty. Możemy też dowolnie zmieniać to, co jest w pudełku.

Pamiętaj, że na samym początku musisz złożyć pudełko (zdefiniować zmienną) i napisać coś na etykiecie (nadać nazwę zmiennej).

W naszej grze będziemy potrzebowali zmiennych m.in. do przechowywania zagrania komputera. Już za chwilę będą wybierane w oparciu o algorytm skryptu – najpierw jednak przypiszemy do zmiennej jedną wartość, wpisaną na stałe.


W kodzie JS powyższego przykładu najpierw deklarujemy zmienną za pomocą słowa let. Deklarujemy, czyli mówimy "będę używać zmiennej o nazwie computerMove". Następnie przypisujemy jej wartość 'kamień'. W ostatniej linii wykorzystujemy zmienną zamiast słowa 'kamień'.

Zauważ, że przy wykorzystaniu zmiennej nie zamykamy jej w cudzysłowach, czyli zamiast 'kamień' wstawiamy computerMove.

Dlaczego tak dziwnie nazywamy zmienne i funkcje?

Dlaczego computerMove, a nie computer move? Nazwa zmiennej musi być jednym "wyrazem", czyli nie może zawierać spacji. Może składać się tylko ze znaków alfanumerycznych (cyfr i liter, ale bez polskich znaków). Wyjątkiem jest znak podkreślenia, czyli _, który jest traktowany jak litera. W takim razie moglibyśmy nazwać tę zmienną computer_move. Dlaczego nie użyjemy tej nazwy?

Tego typu decyzje często wynikają z ogólnie przyjętej konwencji. Np. w języku PHP stosuje się właśnie nazwy zmiennych z podkreśleniem. W JavaScripcie największą popularność ma tzn. zapis "camelCase", w którym słowa są zlepione ze sobą, i każde kolejne zaczyna się od wielkiej litery.

Kolejnym standardem stosowanym przez większość programistów JS jest używanie wyłącznie języka angielskiego do wszystkiego, poza tekstami, które mają być wyświetlone użytkownikowi. Właśnie dlatego computerMove, a nie ruchKomputera.

Zadanie: Wykorzystanie zmiennej

W ostatnim przykładzie zapisaliśmy słowo 'kamień' w zmiennej computerMove. Następnie, wykorzystaliśmy tę zmienną w tekście wyświetlanym na stronie.

Twoim zadaniem jest wykonanie takiej samej zmiany dla słowa 'papier', które zapiszesz w innej zmiennej – playerMove.

Po wykonaniu zadania umieść kod JS w pliku js/script.js, zapisz commit i wykonaj push. Następnie na stronie GitHuba znajdź w swoim projekcie najnowszy commit i wyślij mentorowi link do tego commita.

Po czym poznać link do commita?

Poprawny link będzie miał składnię:

https://github.com/USER/REPO/commit/COMMIT_IDENTIFICATOR

Np. zobacz link do jednego z commitów w repozytorium bootstrap użytkownika twbs:

https://github.com/twbs/bootstrap/commit/8437be2f335ff4e9228c08ce85bf858e0e29324d

5.4. Wybieranie ruchów

Do tej pory komputer zawsze wybierał "kamień", więc kolejne rundy były – delikatnie mówiąc – łatwe do przewidzenia. W tym submodule zajmiemy się losowaniem ruchu komputera oraz umożliwieniem graczowi wyboru własnego ruchu.

Losowanie liczby

Czas na odrobinę matematyki. Będziemy losować ruch komputera, aby nie był to za każdym razem kamień. W JS istnieje wbudowana funkcja pozwalająca na losowanie liczb – jest to Math.random. Ta funkcja losuje liczbę z zakresu od 0 do 0.999... ("prawie 1"). Ten zakres może wydawać się dziwny, ale za chwilę zobaczysz, jak bardzo jest przydatny.

No dobra, ale co zrobić z jakimś wylosowanym ułamkiem, żeby dostać jedno z trzech możliwych zagrań? Zacznijmy od wylosowania liczby 1, 2 lub 3. Skoro Math.random losuje liczbę od 0 do 0.999..., to wystarczy:

  • pomnożyć wylosowaną liczbę przez 3, co da nam liczbę z przedziału od 0 do 2.999... ("prawie 3"),
  • do tej liczby dodać 1, co da nam liczbę z przedziału od 1 do 3.999... ("prawie 4"),
  • zaokrąglić wylosowaną liczbę w dół za pomocą Math.floor, co da nam liczbę od 1 do 3.

Ten "dziwny przedział" losowej liczby sprawia, że otrzymamy losowy wybór jednej z 3 liczb.

Dla zainteresowanych matematyką

Dzięki temu, że Math.random losuje liczbę z zakresu od 0 do 0.999... ("prawie 1"), możemy wylosowany zakres zaokrąglić w dół, otrzymując zawsze 0. Jeśli dołożymy mnożenie i dodawanie, wedle powyższego schematu, będzie równa szansa na wylosowanie liczb 1, 2 i 3.

Gdyby Math.random losował liczbę z zakresu od 0 do 1, to w zależności od podejścia, albo mielibyśmy bardzo rzadki przypadek wylosowania liczby 4, albo liczba 2 wypadałaby dwukrotnie częściej od pozostałych.

Mamy już algorytm losowania, czas zamienić go na kod!


Przy każdym wykonaniu skryptu zostanie wylosowana nowa liczba. Oczywiście, nasza finalna liczba czasami będzie taka sama liczba kilka razy z rzędu – szybko to zauważysz, ponieważ są tylko trzy możliwe liczby.

Jak już się zapewne domyślasz, w powyższym kodzie znak * oznacza mnożenie, a znak + oznacza dodawanie. Warto zauważyć, że znak + służy w JS zarówno do dodawania, jak i łączenia tekstów. Może to czasem prowadzić do błędów, z którymi spotkasz się w kolejnym module. Pokażemy Ci wtedy jak ich unikać.

W powyższym przykładzie zapisaliśmy osobno losowanie liczby, operacje matematyczne, oraz zaokrąglenie. Zrobiliśmy to, aby łatwiej było Ci zrozumieć kolejne kroki, jednak moglibyśmy zapisać całość jako:

let randomNumber = Math.floor(Math.random() * 3 + 1);

Ćwiczenie

Zmień powyższy kod tak, aby dodatkowo losował liczbę całkowitą od 11 do 19.

Przetestuj swój kod, aby upewnić się, że możliwe jest wylosowanie liczb 11 i 19, ale nigdy nie zostanie wylosowana liczba spoza tego zakresu (np. 7 lub 23).

Jeśli okaże się, że Twój kod działa nieprawidłowo, zastanów się, czy w naszym przykładzie liczba 3 oznacza maksymalną wartość, czy może raczej liczbę możliwych rezultatów.

Nie musisz zapisywać swojego rozwiązania – możesz wykonać to ćwiczenie w zagnieżdżonym edytorze powyżej.

Logika warunkowa if...else

Dość matematyki – czas na trochę logiki. Mamy już wylosowaną liczbę 1, 2 lub 3 i teraz chcemy na tej podstawie wybrać jeden z ruchów: "kamień", "papier", lub "nożyce".

Wykorzystamy do tego wyrażenie if...else, które – w wielkim skrócie – działa na zasadzie "jeśli warunek jest spełniony, zrób to, a w przeciwnym wypadku, zrób tamto".

Już za chwilę nie będziesz sobie wyobrażać świata bez if...else. Jest to podstawa programowania, ponieważ pozwala na to, aby Twój skrypt rozpoznawał okoliczności, w których ma (lub nie ma) wykonać pewne operacje.

W naszej grze if...else pozwoli nam nie tylko na to, aby np. na podstawie wylosowanej liczby 1 otrzymać słowo "kamień". Użyjemy go też do sprawdzania, kto wygrał daną partię. Zacznijmy jednak od podstaw...

Metafora - if...else

Strukturę if...else możemy wyobrazić sobie jako sortownię poczty, która w zależności np. od rozmiaru czy wagi przesyłek kieruje je do różnych ciężarówek. W tej sortowni jest jednak zasada, że można zadawać tylko pytania, na które odpowiedź to "tak" lub "nie". Na przykład:

  • Czy paczka ma naklejkę "ostrożnie – szkło"? Jeśli tak, musi jechać ciężarówką A.
  • Czy paczka jest standardowych rozmiarów? Jeśli tak, musi jechać ciężarówką B.
  • Czy paczka waży ponad 10 kg? Jeśli tak, musi jechać ciężarówką C.
  • W przeciwnym wypadku musi jechać ciężarówką D.

Zobaczmy działanie if...else na przykładzie!

if(1 > 2){
	printMessage('Niesamowite! Jeden jest większe niż dwa!!!');
} else {
	printMessage('Wszystko po staremu, jeden jest mniejsze niż dwa.');
}

Oczywiście, w tym przykładzie sprawdzamy, czy 1 jest większe niż 2, tylko po to, aby zaprezentować, jak działa wyrażenie if...else. Ci z Was, którzy uwielbiają matematykę, zapewne już zdążyli zauważyć nasz błąd – przecież jeśli nie jest prawdą 1 > 2, to wcale nie znaczy, że "jeden jest mniejsze niż dwa". A co, jeśli by były równe? Tu z pomocą przyjdzie nam else if.

if(1 > 2){
	printMessage('Niesamowite! Jeden jest większe niż dwa!!!');
} else if (1 == 2) {
	printMessage('Dziwne – jeden jest równe dwa?!');
} else {
	printMessage('Wszystko po staremu, jeden jest mniejsze niż dwa.');
}

Można stosować wiele bloków else if, a dopiero na końcu można (ale nie trzeba) użyć bloku else. Pamiętaj, że jeśli spełniony zostanie pierwszy warunek (1 > 2), to drugi w ogóle nie zostanie sprawdzony. Tylko kiedy pierwszy warunek będzie fałszywy, sprawdzony zostanie drugi (1 == 2). Jeśli on też będzie fałszywy, zostanie wykonany kod w bloku else.

Zauważ, że do sprawdzenia, czy dwie liczby są sobie równe, użyliśmy wyrażenia ==, czyli dwóch znaków równości. W ten sam sposób możemy sprawdzać, czy dwa teksty są identyczne!

To, że użyliśmy dwóch znaków równości, a nie jednego, jest bardzo ważne (i jest źródłem niezliczonej liczby siwych włosów u początkujących programistów). Bardzo łatwo jest, zamiast == niechcący napisać =, co jest wyrażeniem przypisania wartości do zmiennej. Właśnie za pomocą = ustawialiśmy wartość 'kamień' dla zmiennej computerMove.

let computerMove = 'kamień';

Podsumowując, = i == są zupełnie różnymi wyrażeniami, które niestety wyglądają bardzo podobnie. W kolejnym module przypomnimy o tym i pokażemy, jakie problemy mogą wynikać z tego błędu.

Łączymy losowanie i logikę warunkową

Umiesz już wylosować liczbę, ale w naszej grze potrzebujemy wyświetlić nazwę ruchu, który został wylosowany. Będzie to możliwe dzięki połączeniu wylosowanej liczby z logiką warunkową.


Powyżej wykorzystaliśmy ten sam jednolinijkowy kod losowania liczby, który pokazywaliśmy wcześniej w tym module. Pod nim zapisaliśmy początkową wartość computerMove równą 'nieznany ruch', a następnie dodaliśmy warunek, który sprawdza, czy wylosowano liczbę 1 – jeśli tak, to zmienna computerMove będzie miała nową wartość: 'kamień'.

A co jeśli wylosowana liczba będzie inna? Dlaczego wtedy wyświetla się "nieznany ruch"? Rozwiązanie tego problemu już za chwilę będzie Twoim zadaniem!

Konsola i console.log

Bardzo często w trakcie pisania kodu będziemy korzystać z funkcji console.log, aby debugować nasz kod i lepiej rozumieć jak działa.

Jak widzisz, console.log nie wyświetla niczego na stronie. Zamiast tego, komunikaty wyświetlane za pomocą tej funkcji znajdziesz w konsoli znajdującej się wśród narzędzi developerskich.

Otwórz narzędzia developerskie – możesz to zrobić za pomocą klawisza F12 lub kliknąć prawym przyciskiem na dowolny element na stronie i wybrać "Zbadaj element". Następnie, w panelu narzędzi developerskich, przełącz się z zakładki Elements na Console.

image

Dzięki zastosowaniu console.log nie będziemy zaśmiecać sobie widoku gry. Wszelkie informacje, które pomogą nam w rozumieniu kodu lub rozwiązywaniu problemu będziemy od teraz wyświetlać w konsoli.

Zapisywanie odpowiedzi gracza

Mamy już wylosowane zagranie komputera, teraz czas na interakcję z graczem. Dzięki temu będzie mógł wybrać swój ruch.

Wykorzystamy do tego funkcję prompt, która wyświetli okienko z polem na wpisanie wartości. Dla uproszczenia poprosimy gracza o wpisanie liczby 1, 2 lub 3.

Poniższy przykład wyświetli to okienko bezpośrednio po uruchomieniu kodu. Dlatego jego kod będzie widoczny dopiero po kliknięciu "Run Pen".


Ten kod wygląda bardzo podobnie do wcześniejszego, w którym losowaliśmy ruch komputera. Różni się jedynie nazwami zmiennych oraz sposobem ustalenia liczby 1-3. W przypadku komputera losowaliśmy tę liczbę, a w tym przypadku liczbę podaje gracz.

Podobnie jak wcześniej, skrypt rozpoznaje tylko wybór kamienia – pozostałe ruchy pozostają jako "nieznany ruch". Za chwilę jednak rozwiążesz również ten problem!

Zaczynamy zbliżać się do pierwszej wersji, którą będzie już można nazwać grą. Na razie losujemy ruch komputera i pytamy gracza o jego ruch. W następnym submodule zajmiemy się ustaleniem, kto wygrał rozgrywkę!

Zadanie: Dokończenie logiki gry

Twoim zadaniem jest dokończenie gry tak, aby poprawnie rozpoznawała ruch komputera i człowieka, a także wyświetlała wynik rundy!

Zacznij od usunięcia całej zawartości pliku js/script.js.

Odczytanie ruchu komputera

Powyżej w jednym z zagnieżdżonych edytorów pokazaliśmy kod, który losuje liczbę od 1 do 3, i jeśli ta liczba to 1, rozpoznaje ruch jako "kamień".

Skopiuj cały kod z tego edytora do pliku js/script.js, a następnie uzupełnij go o dwa bloki else if. Powinny one obsłużyć przypadki wylosowania liczby 2 i 3. W tych sytuacjach zmienna computerMove ma otrzymać odpowiednią wartość – 'papier' lub 'nożyce'.

Po wykonaniu tej części zadania zapisz commit i wyślij go do zdalnego repozytorium.

Odczytanie ruchu gracza

Znajdź teraz edytor, w którym użyliśmy funkcji prompt do zapytania gracza o jego ruch. Skopiuj cały kod JS z tego przykładu i umieść go na końcu pliku js/script.js.

Podobnie jak wcześniej, dodaj bloki else if dla ruchów "papier" i "nożyce".

Po wykonaniu tej części zadania ponownie zapisz commit i wyślij go do zdalnego repozytorium.

Wynik gry

Na końcu pliku js/script.js dodaj teraz nowy blok if...else if...else, który wyświetli komunikat o wyniku gry.

Dla przykładu, jeśli computerMove to 'kamień', a playerMove to 'papier', powinien zostać wyświetlony komunikat "Ty wygrywasz!".

W rozwiązaniu tego zadania będzie Ci potrzebny sposób na sprawdzenie, czy spełnione są oba warunki. Najwygodniej będzie to zrobić za pomocą operatora &&, który oznacza "oraz".

if( computerMove == 'kamień' && playerMove == 'papier'){
  printMessage('Ty wygrywasz!');
}

Pamiętaj, żeby obsłużyć również sytuację, gdy występuje remis, a także, kiedy playerMove jest równe nieznany ruch – taka sytuacja może mieć miejsce, jeśli gracz nie posłuchał polecenia i wpisał coś innego niż 1, 2 lub 3.

Po zakończeniu pracy znów zapisz commit i wyślij go do zdalnego repozytorium. Następnie, link do najnowszego commita wyślij do sprawdzenia.

Powodzenia!

5.5. Poprawa kodu projektu

Udało nam się już zrobić pierwszą wersję gry! Jednak czy nasz skrypt jest dobrze napisany? Jak już zapewne się domyślasz (a na pewno pamiętasz z poprzednich modułów!) powtarzanie prawie identycznego kodu nie jest dobrą praktyką. Zasada DRY (Don't Repeat Yourself) działa również w JS. Dlatego chcielibyśmy uniknąć wykorzystania tej samej logiki if/else dwa razy (dla zagrania komputera oraz gracza). Tu z pomocą przyjdą nam funkcje!

Czym jest funkcja?

Jak wspomnieliśmy na samym początku pisania gry, funkcje to pewne "procedury". Na razie nauczyliśmy się tylko wywoływać funkcje, np. w ten sposób:

printMessage('Hello world!');

Zanim przejdziemy do tworzenia własnych funkcji, musimy dokładniej wytłumaczyć, czym jest funkcja i do czego może nam się przydać.

Funkcja może:

  • coś przyjmować – funkcja często potrzebuje jakichś informacji, w oparciu o które będzie działać, np. funkcja printMessage musi dostać tekst, który ma zostać wyświetlony,
  • coś robić – funkcja wykonuje pewne operacje, które mogą (ale nie muszą) mieć wpływ na cokolwiek poza funkcją, np. funkcja printMessage powoduje wyświetlenie tekstu na stronie,
  • coś zwracać – funkcja może wstawiać jakąś "odpowiedź" w miejsce jej wywołania (np. funkcja prompt zwraca odpowiedź wpisaną przez użytkownika), dzięki czemu zwróconą wartość możemy np. zapisać do zmiennej lub wykorzystać w innym wyrażeniu.

Zwróć uwagę, że różne funkcje będą posiadać różne kombinacje powyższych punktów, czyli np. jakaś funkcja może coś przyjmować i coś wykonywać, ale niczego nie zwracać. Dokładnie tak się dzieje w przypadku funkcji printMessage.

Metafora – funkcja

Możesz sobie wyobrazić funkcję, jako maszynę. Są różne maszyny – np. pralki, windy, czy nawet samoloty. W wielu przypadkach nie wiesz, jak taka maszyna działa, i nie musisz tego wiedzieć, aby z niej korzystać. W przypadku naszej metafory będzie tak samo w niektórych przypadkach, natomiast w innych będziemy sami budować naszą maszynę.

Weźmy na przykład pralkę. Musisz coś do niej włożyć (brudne ubrania, proszek), bo bez tego jej działanie nie ma sensu. Pralka potem coś robi przez jakiś czas, a na końcu dostajemy z powrotem czyste ubrania.

W przypadku funkcji będziemy dostarczać informacje (liczby, teksty, etc.), które nazywamy argumentami, a w rezultacie funkcja zwróci nam coś innego (inne liczby, inne teksty).

Innym rodzajem maszyny jest winda. Tu nie musimy niczego dostarczyć ani niczego nie dostaniemy w zamian. Za to winda wykona dla nas jakąś pracę.

Istnieją także funkcje, którym nie podajemy argumentów i które niczego nie zwracają. Będą one jednak wykonywać jakąś operację (np. zmieniać kolor tła na stronie).

Najważniejsze jest jednak, że tej maszyny możemy używać wielokrotnie. Niektóre maszyny zachowają się za każdym razem tak samo, inne – w zależności od tego, co im dostarczymy. Tak czy inaczej, nie musimy budować nowej pralki do każdego prania.

Pamiętaj też, że samo kupienie pralki nie sprawi, że Twoje ubrania będą czyste! Podobnie i funkcja musi być stworzona (zadeklarowana), ale zrobi coś, dopiero kiedy ją uruchomimy (wywołamy).

Opiszmy sobie kilka znanych już nam funkcji wedle powyższego podziału na przyjmowanie argumentów, wykonywanie operacji poza funkcją, oraz zwracanie wartości. Pozwoli nam to lepiej zrozumieć, do czego możemy wykorzystywać funkcje:

  • printMessage przyjmuje argument tekstowy, wykonuje dodanie tego tekstu na stronie, i nie zwraca żadnej wartości,
  • prompt przyjmuje opcjonalny argument tekstowy do wyświetlenia w oknie prompta, wykonuje wyświetlenie okna z polem tekstowym, oraz zwraca tekst wpisany w to pole (lub null po wciśnięciu guzika "Anuluj"),
  • Math.random nie przyjmuje żadnych argumentów, niczego nie wykonuje, ale zwraca losową liczbę z przedziału od 0 do 0.999...,
  • Math.floor przyjmuje argument w postaci liczby, nie wykonuje żadnych operacji, i zwraca liczbę podaną w argumencie po zaokrągleniu w dół.

Niektóre funkcje, z których będziemy korzystać, są wbudowane w przeglądarkę – np. prompt czy Math.random. Oznacza to, że zostały już zadeklarowane gdzieś w kodzie Twojej przeglądarki i nie trzeba ich deklarować ponownie. Często jednak będziemy pisać własne funkcje, aby nie wklejać wielokrotnie tego samego kodu.

Przykładem takiej funkcji może być printMessage – została ona zadeklarowana w pliku js/functions.js. Wykorzystujemy w niej jednak operacje, które omówimy dopiero za chwilę. Dlatego zaczniemy od napisania własnej funkcji!

Pamiętaj, że będziemy mieli do czynienia z dwoma operacjami: deklarowaniem i wykonywaniem funkcji. Samo zadeklarowanie funkcji jest tylko zapisaniem algorytmu, który ma być przez nią wykonany. Dopiero po wywołaniu tej funkcji algorytm zostanie wykonany.

Aby ułatwić Ci zrozumienie kodu, nazwy wszystkich argumentów będziemy na razie zaczynać od prefiksu arg. Nie jest to wymogiem, ani nawet standardem, ale przy pierwszych krokach w JavaScripcie bywa to bardzo pomocne.

Tworzenie funkcji

Dość teorii, wracamy do kodu! Dla ćwiczenia zaczniemy od prostej funkcji, która będzie obliczać resztę, którą kasjer w sklepie musi wydać klientowi.


Spójrz na powyższy kod JS i znajdź w nim deklarację funkcji calculateChange, która zaczyna się od słowa function. Zanim przejdziesz dalej, spróbuj odpowiedzieć sobie na pytania:

  • Ile argumentów przyjmuje ta funkcja?
  • Jaką wartość zwróci (ang. return) ta funkcja?

Znajdź również miejsca, w których funkcja calculateChange zostaje wykonana. Pierwsze dwa przykłady są bardzo podobne – zapisujemy zwracaną wartość do zmiennych. Za to w trzecim wykonujemy funkcję od razu w miejscu, w którym jej wynik ma być wstawiony do tekstu wyświetlanego na stronie. Oba podejścia są poprawne, przy czym pierwsze (zapisanie wyniku do zmiennej) jest lepsze, jeśli będziemy wykorzystywać tę wartość więcej niż raz. Np. gdybyśmy chcieli zsumować obliczone kwoty ze wszystkich przykładów, lepiej byłoby zapisać do zmiennych wyniki zwracane przez funkcję, aby nie wykonywać tych samych obliczeń ponownie.

Wróćmy jednak do deklaracji funkcji. Jak już zapewne się domyślasz, deklaracja funkcji ma składnię:

function nazwaFunkcji(argument){
  return zwracanaWartosc;
}

Przyjrzyj się poniższym przykładom i odpowiedz sobie na pytania:

  • Ile argumentów ma ta funkcja? Może nie mieć żadnego, mieć jeden, lub więcej.
  • Jakie operacje wykona ta funkcja? Czy będzie miała wpływ na cokolwiek poza zwracaną wartością?
  • Czy funkcja coś zwraca? Jeśli tak, od czego zależy zwracana wartość?
  • Jak możesz wykorzystać te funkcje w swoim projekcie?
function getMoveName(argMoveId){
  if(argMoveId == 1){
    return 'kamień';
  } else {
    printMessage('Nie znam ruchu o id ' + argMoveId + '.');
    return 'nieznany ruch';
  }
}

function displayResult(argComputerMove, argPlayerMove){
  printMessage('Zagrałem ' + argComputerMove + ', a Ty ' + argPlayerMove);

  if( argComputerMove == 'kamień' && argPlayerMove == 'papier'){
    printMessage('Ty wygrywasz!');
  } else {
    printMessage('Tym razem przegrywasz :(');
  }
}

Pamiętaj, że powyższy kod niczego nie wykona, ponieważ zawiera tylko deklaracje funkcji. Te funkcje nie zostały jeszcze wykonane!

Zwróć uwagę, że wewnątrz tych funkcji wykorzystaliśmy wyrażenia, które są nam już dobrze znane. Jedyną nowością jest return – wyrażenie, które sprawi, że funkcja zwróci wartość podaną bezpośrednio po nim.

Warto wiedzieć, że return kończy wykonanie funkcji. Dlatego w powyższej funkcji getMoveName, w bloku else musieliśmy najpierw wykonać funkcję printMessage, a dopiero potem zwrócić wartość za pomocą return. Oznacza to też, że moglibyśmy zapisać tę funkcję w ten sposób:

function getMoveName(argMoveId){
  if(argMoveId == 1){
    return 'kamień';
  }

  printMessage('Nie znam ruchu o id ' + argMoveId + '.');
  return 'nieznany ruch';
}

Jeśli argument argMoveId będzie równy 1, to zostanie zwrócona wartość 'kamień' i zakończy się wykonanie funkcji, więc nie zostanie wykonana funkcja printMessage.

Zastanówmy się teraz, do czego będą nam potrzebne funkcje getMoveName oraz displayResult.

  • Funkcja getMoveName będzie wykorzystywana zarówno w przypadku ruchu gracza, jak i ruchu komputera. Dzięki zastosowaniu funkcji nie będziemy powtarzać dwukrotnie tego samego kodu.
  • Funkcję displayResult wykorzystamy tylko raz, ale służy nam do uporządkowania naszego kodu. Dzięki temu cała logika decydująca o wyniku gry jest zawarta w jednym miejscu i oddzielona od reszty kodu.

Zadanie: Wykorzystanie funkcji

Twoim zadaniem jest uzupełnienie i wykorzystanie funkcji getMoveName oraz displayResult w taki sposób, aby gra działała poprawnie.

W uzupełnieniu deklaracji tych funkcji pomoże Ci rozwiązanie poprzedniego zadania, ponieważ przenosimy do tych funkcji logikę, którą już napisaliśmy.

Wykorzystanie funkcji getMoveName

Zacznij od wstawienia tej funkcji na początku pliku js/script.js. Teraz zajmiemy się jej wykorzystaniem w dotychczasowym kodzie projektu. Pamiętaj, że kod tej funkcji będzie wymagał uzupełnienia, co zrobimy za chwilę.

Znajdź fragment kodu, w którym deklarujesz zmienną computerMove:

let computerMove = 'nieznany ruch';

Po tej linii znajdziesz wyrażenie if...else if..., które zmienia wartość zmiennej computerMove w zależności od wartości zmiennej randomNumber.

Zacznij od zamknięcia całego wyrażenia if...else if... w komentarz blokowy, czyli taki który może obejmować wiele linii. Zrobisz to, wpisując /* w miejscu rozpoczęcia komentarza oraz */ w miejscu jego zakończenia. Dzięki temu ten fragment kodu nie będzie działał, ale pozostanie w pliku – będzie nam za chwilę potrzebny do uzupełnienia funkcji getMoveName.

Teraz zmień deklarację zmiennej computerMove tak, aby zapisać w niej wartość zwracaną przez funkcję getMoveName:

let computerMove = getMoveName(randomNumber);

Możesz teraz przetestować czy wszystko działa poprawnie. Pamiętaj tylko, że jeszcze nie uzupełniliśmy deklaracji funkcji getMoveName, więc będzie ona zwracać jedynie 'kamień' lub 'nieznany ruch'.

Następnie w analogiczny sposób zmień deklarację zmiennej playerMove oraz zakomentuj (czyli zamknij w komentarzu) wyrażenie if...else if..., które modyfikuje wartość tej zmiennej.

Ostatnim krokiem będzie uzupełnienie deklaracji funkcji getMoveName. Wzorując się na zakomentowanym fragmencie kodu, spraw by funkcja zwracała 'papier' lub 'nożyce', kiedy argument będzie miał wartość 2 lub 3.

W rezultacie gra powinna działać tak samo, jak przed wykonaniem tego zadania, ale zamiast osobnych wyrażeń if...else if... dla ruchów komputera i gracza, teraz mamy tylko jedno – w deklaracji funkcji getMoveName.

Wykorzystanie funkcji displayResult

Tym razem nie będziemy kopiować funkcji z przykładu pokazanego wcześniej w tym submodule. Zamiast tego napisz samodzielnie deklarację funkcji displayResult. Ma ona przyjmować dwa argumenty, które nazwiemy argComputerMove i argPlayerMove.

Następnie do deklaracji tej funkcji przenieś fragment kodu odpowiedzialny za wyświetlenie wyniku gry. Musisz go zmodyfikować w taki sposób, aby zamiast zmiennych computerMove i playerMove wykorzystywał argumenty argComputerMove i argPlayerMove.

Pamiętaj, aby w miejscu usuniętego kodu wstawić wywołanie funkcji displayResult!

Rozwiązywanie problemów

W trakcie wykonywania zadania możesz napotkać problemy. Aby sobie z nimi poradzić, używaj console.log do sprawdzenia co dzieje się w kodzie. Możesz np.:

  • na początku deklaracji którejś funkcji wstawić console.log wyświetlający jakiś tekst, aby sprawdzić, czy ta funkcja w ogóle się wykonuje,
  • użyć console.log do wyświetlenia argumentów funkcji,
  • wstawić console.log do bloków if, else if i/lub else, aby sprawdzić, który z nich zostanie wykonany,
  • sprawdzić co zwraca funkcja, kiedy podamy jej konkretne argumenty, wpisując np. na końcu pliku console.log(getMoveName('2'));,
  • użyć console.log po linii ustawiającej wartość jakiejś zmiennej, aby wiedzieć jaka wartość została w niej zapisana.

Dzięki temu, w konsoli możesz zobaczyć każdy krok działania skryptu. Szczególnie w początkowych etapach nauki, bardzo dobrym podejściem jest dodawanie console.log, nawet jeśli skrypt działa poprawnie, aby lepiej zrozumieć jego funkcjonowanie.

Możesz nam wierzyć, że zwyczaj używania console.log często jest kluczową różnicą pomiędzy developerem, który sprawnie rozwiązuje problemy, a tym, który godzinami rwie włosy z głowy. Dlatego od teraz, na pytanie "dlaczego to nie działa" niech zawsze Twoją odpowiedzią będzie "użyję console.log!" ;)

W sprawnym korzystaniu z console.log może Ci pomóc informacja, że możesz tej funkcji przekazywać wiele argumentów, co jest wygodniejsze niż używanie + do łączenia tekstów. Wszystkie argumenty zostaną wyświetlone w konsoli. Możesz np. dodać taką linię na początku funkcji displayResult:

console.log('moves:', argComputerMove, argPlayerMove);

Po wykonaniu zadania zapisz commit i wyślij jego link do sprawdzenia.

5.6. Interfejs gry

Nasza gra już działa, ale pozostaje problem interfejsu – nie jest zbyt przyjazny użytkownikowi. Gra uruchamia się przy każdym odświeżeniu strony i wymaga, żeby gracz od razu wpisywał swój ruch za pomocą klawiatury. Dlatego teraz zajmiemy się interfejsem gry, aby grało się w nią znacznie przyjemniej!

W początkowej specyfikacji projektu założyliśmy, że na stronie mają znajdować się trzy guziki: "kamień", "papier" i "nożyce". Kliknięcie któregokolwiek z nich ma uruchamiać grę, czyli losować zagranie komputera oraz wyświetlać wynik.

Obsługa kliknięcia guzika

Zacznijmy jednak od prostego przykładu powiązania jakiejś akcji z kliknięciem guzika. Poniższy kod wyświetla komunikat na stronie po kliknięciu guzika. Spróbuj samodzielnie przeczytać kod tego skryptu i zrozumieć jak działa.


Pojawiło się kilka nowości! Po pierwsze, funkcja document.getElementById służy do znalezienia na stronie elementu o id podanym jako argument tej funkcji. Zwróci nam ona odwołanie do tego elementu, które zapisaliśmy w zmiennej testButton.

Jeśli wartością zmiennej jest odwołanie do elementu na stronie, czyli elementu DOM, możemy na niej wykonać jedną z wielu funkcji dostępnych dla elementów DOM. Robimy to, dodając kropkę po nazwie zmiennej, a po niej podajemy nazwę funkcji. W tym przypadku jest to funkcja addEventListener, ale z czasem poznasz dużo więcej funkcji elementów DOM.

Funkcja addEventListener tworzy listener, który będzie nasłuchiwał jakiegoś zdarzenia na tym elemencie. Jako pierwszy argument podajemy jej rodzaj zdarzenia – kliknięcie, czyli 'click'. Drugim argumentem jest funkcja, która ma zostać wykonana, kiedy to zdarzenie się wydarzy.

Spróbujmy przeczytać tę linię kodu:

testButton.addEventListener('click', buttonClicked);

Mówi ona: "za każdym razem, kiedy wykryjesz kliknięcie na elemencie zapisanym w zmiennej testButton, wykonaj funkcję buttonClicked".

Zwróć uwagę, że w naszym skrypcie nie ma ani słowa o tym, że klikany element jest guzikiem! W ten sam sposób moglibyśmy obsłużyć również kliknięcie w obrazek, nagłówek, div, czy jakikolwiek inny element DOM.

Moglibyśmy też zapisać ten sam skrypt w inny sposób, bez nadawania nazwy funkcji i bez zapisywania odwołania do guzika w zmiennej. Wtedy skrypt wyglądałby tak:

document.getElementById('test-button').addEventListener('click', function(){
  printMessage('Guzik został kliknięty');
});

Na początku ten zapis może być dla Ciebie znacznie mniej czytelny, ale szybko się do niego przyzwyczaisz. Bardzo często właśnie w ten sposób zapisuje się listener zdarzenia.

Wykorzystujemy tutaj funkcję anonimową, czyli taką, której nie nadaliśmy nazwy. Zapis function(){} stosujemy m.in. wtedy, kiedy jest nam potrzebna funkcja, której nie będziemy potrzebowali ponownie wykonywać.

Zadanie: Implementacja guzików

Jak już zapewne się domyślasz, celem tego zadania jest dokończenie naszej gry. Na stronie umieścisz trzy guziki i napiszesz kod, który sprawi, że po kliknięciu któregoś z guzika zostanie rozegrana partia gry. Oczywiście, wybór guzika będzie jednocześnie wyborem ruchu gracza, dzięki czemu nie będzie już potrzeby wpisywania 1, 2 lub 3 w oknie prompt.

Przygotowanie gry do wdrożenia guzików

Pierwszym krokiem będzie stworzenie funkcji playGame, w której ma znaleźć się cały kod, który dotychczas znajdował się w pliku js/script.js. Innymi słowy, na początku pliku dodaj function playGame(){, a na końcu pliku }. Następnie popraw wcięcia kodu wewnątrz funkcji.

Teraz po odświeżeniu strony gra nie powinna już się uruchamiać – za to za chwilę będzie się uruchamiać po kliknięciu guzika!

Funkcja playGame powinna przyjmować jeden argument – liczbę 1, 2 lub 3, która będzie odpowiadała ruchowi gracza. W pierwszej linii deklaracji tej funkcji dodaj deklarację argumentu o nazwie playerInput. Następnie wewnątrz funkcji znajdź linię, w której deklarowaliśmy zmienną playerInput i usuń (lub zakomentuj) tę linię. Zamiast niej będziemy teraz wykorzystywać argument funkcji.

Na końcu pliku, czyli poza funkcją, dodaj jej wykonanie:

playGame(3);

Jeśli wszystko poszło dobrze, po odświeżeniu strony powinny wyświetlić się komunikaty z rozegrania gry, przy czym ruchem gracza będą za każdym razem nożyce (odpowiadające liczbie 3 w powyższym kodzie).

Po sprawdzeniu działania funkcji playGame usuń jej wywołanie, aby gra nie uruchamiała się po odświeżeniu strony.

Ostatnim krokiem jest dodanie wywołania funkcji clearMessages (bez argumentów) na samym początku deklaracji funkcji playGame. Ta funkcja, której definicja znajduje się w js/functions.js, usuwa wszystkie wyświetlone do tej pory komunikaty. Dzięki temu będziemy mogli uruchamiać grę wielokrotnie.

Dodanie guzików

W kodzie HTML dodaj teraz trzy guziki do diva o parametrze id="buttons". Każdy z tych guzików ma być elementem <button> i musi mieć jakieś id – mogą to być np. play-rock, play-paper i play-scissors. W treści guzików wpisz nazwy ruchów odpowiadające wartościom idKamień, Papier i Nożyce.

Implementacja listenerów

Opierając się na przykładzie z tego submodułu, dodaj na samym końcu pliku js/script.js trzy listenery – po jednym dla każdego z dodanych przed chwilą guzików. Pamiętaj, aby wstawić ich id zamiast test-button!

Następnie wewnątrz anonimowej funkcji każdego z listenerów zmień treść komunikatu wyświetlanego za pomocą funkcji printMessage. Przetestuj teraz czy każdy z guzików wyświetla odpowiedni komunikat na stronie.

W ostatnim kroku, w każdym z listenerów zamień wywołanie funkcji printMessage na wywołanie funkcji playGame z odpowiednim argumentem.

Jeśli wszystko poszło dobrze, po kliknięciu któregoś z guzika, wyświetli się komunikat z rezultatem gry. Zagranie gracza będzie zależało od tego, w który guzik kliknął, podczas gdy ruch komputera będzie losowy.

Gratulacje! W ten sposób udało nam się dokończyć Twoją pierwszą grę!

Nie zapomnij zapisać commita i wysłać go do zdalnego repozytorium.

5.7. Podsumowanie

Gdybyśmy zaczęli ten moduł od pokazania Ci tej gry, pewnie trudno byłoby Ci uwierzyć, że w tak krótkim czasie nauczysz się samodzielnie napisać taki skrypt w JS-ie.

Zamiast tego, zaczęliśmy ten moduł od wytłumaczenia Ci myślenia algorytmicznego. Ćwiczysz tę umiejętność codziennie, prawda? ;)

Myślenie algorytmiczne jest niezbędne do tego, aby zadanie "napisz grę w papier-kamień-nożyce" rozpisać na poszczególne etapy i kroki realizacji, tak jak zrobiliśmy to w tym module. Jest też niezbędne do tego, aby powyższą instrukcję zamienić w działający kod.

Programistyczny suchar ;)

Przy okazji, mamy też nadzieję, że dzięki informacjom z tego modułu doskonale zrozumiesz poniższy (stary) dowcip :)

Żona wysyła męża-programistę do sklepu i mówi:
– Kup chleb, a jak będą mieli jajka, to kup dziesięć.
Facet wchodzi do sklepu i pyta:
– Czy są jajka?
– Tak, są.
– To poproszę dziesięć chlebów.

Ostatnia lekcja w tym module

Ostatnią rzeczą, którą chcemy Cię nauczyć, jest dodawanie i usuwanie treści z elementu na stronie. Tych zagadnień nie musimy jednak omawiać – wystarczy, że zajrzysz do pliku js/functions.js, którego zawartość podaliśmy Ci na samym początku tego modułu.

Wtedy kod zawarty w tym pliku pewnie był dla Ciebie czarną magią. A teraz? Czy potrafisz zrozumieć, co robią zawarte w nim funkcje? Czy poradzisz sobie – w razie potrzeby – ze zmianą ich kodu, aby działały dla innych elementów na stronie?

Mamy nadzieję, że tak. Celem tego modułu było właśnie nauczenie Ciebie podstaw JS-a na tyle, żeby zrozumienie tych funkcji było dla Ciebie możliwe – a może wręcz proste!

Jeśli masz jeszcze siły i czas, poniżej przedstawiamy kilka sugestii dot. dalszego rozwoju tego projektu.

Dla chętnych

Twoja gra już działa w swojej podstawowej formie, ale zostało jeszcze wiele miejsca na ulepszenia. Poniżej prezentujemy kilka pomysłów – niektóre pozwolą Ci przećwiczyć zdobyte umiejętności, a inne skłonią do samodzielnych poszukiwań odpowiedzi i pogłębiania wiedzy.

Ostylowanie gry

Wiesz już tyle o HTML i CSS, że to będzie sama przyjemność! Spraw, aby Twoja gra prezentowała się elegancko i zachęcająco: dodaj na stronie nagłówek, tekst powitalny, krótką instrukcję obsługi... Ostyluj całość, aby nadać jej indywidualnego charakteru.

Liczenie wygranych rund

Gra w papier-kamień-nożyce zwykle nie kończy się po jednej rundzie. Dodaj w takim razie licznik rund wygranych przez każdą ze stron.

Dodaj osobny div o id="result", w którym będzie wyświetlany wynik w formacie "X - Y", gdzie X to wygrane gracza, a Y to wygrane komputera. Remis nie zmienia wyniku.

Ważna informacja – jak dowiesz się w następnym module, zmienne let "istnieją" w zakresie nawiasów klamrowych { }, w których zostały zdefiniowane. Dlatego te zmienne musisz zadeklarować poza funkcją playGame, aby ich wartość nie była kasowana przy każdym uruchomieniu tej funkcji. Zmieniaj wartość tych zmiennych przy każdym wywołaniu funkcji playGame, w zależności od tego, kto wygrał.

Czy musi być fair?

A może chcesz zmienić grę tak, aby przy losowaniu ruchu komputera nie każdy wynik był równie prawdopodobny? Czy uda Ci się stworzyć taki wariant gry, w której komputer przegrywa w 75% rozegranych rund?

Jeśli tak, to czy masz pomysł, jak można uruchomić dużą liczbę gier, aby przetestować prawdopodobieństwo przegranej komputera?

Dalszy rozwój projektu

W tym module przedstawiamy tylko niezbędne podstawy i tłumaczymy je na przykładach. Wiemy jednak, że w trakcie samodzielnego eksperymentowania może Ci się przydać ściąga z tematów, których jeszcze nie wyjaśniliśmy — dlatego już teraz udostępniamy Ci poradnik JS, który przygotowaliśmy dla naszych Kursantów. Dzięki niemu będziesz mieć pod ręką najważniejsze informacje o JS w skondensowanej formie.

5.8. Quiz powtórkowy

Na koniec tego modułu przygotowaliśmy dla Ciebie quiz powtórkowy. Pomoże Ci on powtórzyć wiedzę z poprzednich modułów.

Odpowiedzi tego quizu nie są nigdzie zapisywane, więc są tylko do Twojej wiadomości. Ten quiz ma Ci posłużyć jako pomoc w nauce – dlatego pod każdym pytaniem znajdziesz guzik, który sprawdzi poprawność Twoich odpowiedzi oraz poda Ci wyjaśnienie zagadnienia poruszanego w tym pytaniu.

1. Zaznacz prawdziwe zdania dotyczące Gita:

Wyjaśnienie

Git jest systemem wersji. Pozwala on na zapisywanie kolejnych wersji projektu – czyli commitów. Każdy commit zawiera tylko te pliki, które do niego dodamy (np. git add index.html). Nawet jeśli spróbujemy dodać wszystkie pliki za pomocą komendy git add ., zostaną dodane tylko pliki, których zawartość zmieniła się względem poprzedniego commita – z pominięciem plików pasujących do filtrów zapisanych w .gitignore.

Wchodząc w techniczne szczegóły, w commitach nie są zapisywane całe pliki, a tylko informacje o tym, co zmieniło się w danym pliku. Oznacza to, że aktualna wersja projektu jest sumą wszystkich zmian zapisanych w commitach (na tym samym branchu).

Dzięki temu usunięcie pliki jest tylko informacją w commicie o tym, że ten plik został usunięty. Nie zostaną skasowane informacje o tym pliku z poprzednich commitów, więc zawsze można go przywrócić – o ile został zapisany we wcześniejszym commicie.

Zdalne repozytorium – w wielkim skrócie – jest zdalną kopią naszego repozytorium lokalnego. Komendy git push i git pull pozwalają na zsynchronizowanie lokalnego i zdalnego repozytorium. Do tego oczywiście jest wymagane połączenie z internetem.

Do wykonania pusha (a w przypadku repozytorium prywatnego, również pulla) musimy udowodnić, że mamy prawo do zapisu (i ew. odczytu) w tym repozytorium. Moglibyśmy za każdym razem podawać login i hasło, jednak znacznie wygodniej jest korzystać z kluczy SSH, które pozwolą nam na łączenie się z repozytorium zdalnym bez logowania.

Kiedy jesteśmy offline, możemy nadal korzystać z lokalnego repozytorium do zapisywania commitów, tworzenia i przełączania się pomiędzy branchami, porównywania commitów, etc. – czyli brak internetu uniemożliwia nam tylko synchronizację ze zdalnym repozytorium, ale nie blokuje nam pracy nad projektem.

2. W pracy z Gitem, szczególnie w zespole projektowym, bardzo ważne jest przestrzeganie dobrych praktyk nazewnictwa commitów. Zaznacz te commity, które spełniają dobre praktyki.

Wyjaśnienie

Na tym etapie może się to wydawać abstrakcyjne, ale warto dbać o nazwy commitów i częste zapisywanie commitów. Dzięki temu nie tylko lepiej przygotujesz się do pracy nad projektem zespołowym, ale też oszczędzisz sobie sporo nerwów, mogąc wrócić do konkretnej wersji projektu.

Dlatego przypominamy zasady dotyczące tytułów commitów:

  • piszemy po angielsku i zwięźle (do ok. 50 znaków),
  • w miarę konkretnie opisujemy, co zostało zmienione, unikamy ogólnikowych opisów w stylu "Add styles",
  • pierwsze słowo tytułu to czasownik w czasie teraźniejszym prostym, np. "Add", "Fix", "Remove" czy "Change".

Jeśli commit wymaga szerszego wyjaśnienia, możemy je dodać jako opis commita. Wtedy nie używamy komendy git commit -m "Add multiple changes", tylko git commit. W edytorze opisu w 1. linii wpisujemy tytuł commita, 2. linię pozostawiamy pustą, a w kolejnych liniach wpisujemy dowolny opis.

3. Zaznacz zdania poprawnie wyjaśniające komendy gita:

Wyjaśnienie

Commit zapisujemy za pomocą git commit, a wysłanie plików na zdalne repozytorium to git push. Pozostałe komendy opisane powyżej są poprawne.

4. NPM pozwala nam na:

Wyjaśnienie

NPM to menedżer pakietów Node.js, który pozwala na instalację wielu różnych pakietów. Niektóre z nich to narzędzia przydatne developerom, inne mogą być kluczowym elementem projektu, bez którego nie da się go uruchomić.

Wszystkie instalowane pakiety mogą być automatycznie zapisywane w pliku package.json, który powinien znaleźć się w repozytorium. Dzięki niemu inni developerzy (a również Ty na innym komputerze) będą mogli jedną komendą zainstalować wszystkie zależności.

Co więcej, skrypty NPM pozwalają nam na zapisanie (najczęściej) skomplikowanej komendy pod prostą nazwą. Dzięki temu możemy wykonać szereg operacji np. za pomocą komendy npm run build.

Pamiętaj jednak, że pakiety zainstalowane przez NPM nie powinny znaleźć się w repozytorium. Są one instalowane w katalogu node_modules, który należy dodać do .gitignore.

5. Zaznacz prawidłowe zdania dotyczące task runnera:

Wyjaśnienie

Task runner to ogólne pojęcie na narzędzie, które uruchamia inne narzędzia w określonych scenariuszach (taskach). Używanie task runnera jest bardzo popularne i ciężko dzisiaj znaleźć projekt, który nie korzystałby z żadnego task runnera.

Nawet jeśli w różnych projektach używasz różnych narzędzi, możesz ujednolicić sposób ich uruchamiania. Bardzo częstymi nazwami tasków są:

  • init – zainicjowanie projektu,
  • watch – działa w tle, pozwalając na pracę nad projektem,
  • build – buduje wersję projektu, która jest zoptymalizowana do opublikowania,
  • test – uruchomienie testów sprawdzających poprawność kodu.

Warto zauważyć, że task watch zwykle działa w tle, czyli pozostaje uruchomiony i czeka na zmiany w plikach projektu. W reakcji na nie może np. na nowo skompilować pliki lub odświeżyć podgląd projektu.

;